看雪.京东 2018 CTF 第十二题 破解之道 点评与解析
持续的高温天气下的我:
唯一能续命的就是,坐在空调房里吹着冷气,吃着西瓜,看CTF 比赛!
第十二题作者cvcvxk以被16人攻破的成绩,获得第6名的成绩
第12题过后,攻击方
jackandkx在本题表现突出,上升4位,从第9名升至第5名。
距离比赛还有三题!
所剩机会不多了,
谁能最终亲临颁奖典礼现场?
真的是非常期待啊!
看雪版主&评委 netwind 点评
本题设计新颖,没有作任何反调试,依据C++17/14/11的语法特性展开,基本杜绝了IDA静态分析的可能性,以考察选手动态调试能力。本题还考察选手对x64调试器的使用熟练度以及对系统常见动态库的熟悉度。
看雪.京东 2018 CTF 第十一题 作者简介
cvcvxk
bbs.pediy.com/user-48080
第十二题出题者简介:
cvcvxk,目前就职大连暗泉,从事游戏安全,工控安全,区块链安全等相关工作。
看雪.京东 2018 CTF 第十二题 设计思路
参赛题目:Window Crackme
运行平台:win7 x64,纯64位PE
题目答案:KXCTF20189NTDLL9DbgUiContinue9
注册方式:
Crackme.exe 注册码
注册成功的提示:
设计:
1)首先是验证输入长度 30,验证其中9出现的次数
2)验证头9个输入字符是否符合规则,因为是单个字母的hash对比,可以根据hash算法穷举(单个字符只有26*2+10种hash,简单就能爆破)
3)验证整体字符hash,此处设计+30长度的杜绝了爆破
4)分割字符1,第一个9后的5个字母全部复制作为DLL名称,考察怼系统动态库熟悉程度
5)分割字符2,第二个9到第三个9之间作为API名称,考察pe文件导出表函数查找能力(长度为13的API函数固定dll导出的名称收集)
6)PE搜索API加载dll
7)PE搜索API从DLL获取API
8)调用API,因为传入参数的原因DbgUiContinue必然返回一个小于0的值
9)验证返回值<0就输出注册成功
10)本题没有作任何反调试工作,只是进行了大量的C++17/14/11的语法特性展开,基本杜绝了IDA静态分析的可能性,以考察选手动态调试能,64位exe 考察选手x64调试器的使用熟练度。
答题方法:
1)动态调试找到总长度30,和必须含有三个9
2)继续调试找到9个字母的hash和hash算法,用算法生成62个可输入的字符hash直接查表得到9个字符(如果需要代码,请说明)
3)先暂时patch掉整体Hash验证,以进行下面的分析
4)分析分割得到5个字母长度被LoadLibraryA或者GetModuleHandleA获取的DLL过程,
熟悉系统DLL名称,猜到是NTDLL(考察选手系统常见动态库的熟悉度)
5)然后是根据后面的字符长度,找到NTDLL导出的API名字(PE文件),逐个带入验证整体hash
6)这时就可以得到完整的key
7)至此就解题完毕。
看雪.京东 2018 CTF 第十二题解析——在纸老虎的海洋中寻求破解之道
本解析来自看雪论坛 holing
一道纸老虎的题目,关键校验函数F5出来有2k多行,看起来可怕难逆,实际上很多没用的代码,解题关键在于定位有用的代码。
随便翻一下发现0x140002E00这个函数很像关键校验函数,F5一看,很可怕,2400多行。xref一下,发现调用他的很可能是main函数,动态调试发现的确是(通过命令行参数识别,这题输入是命令行参数)。再看看校验函数,一开始觉得这题估计做不出了。(做题靠猜233)
(话说IDA没法自动识别64位PE的main函数的吗?还是做了混淆?)
看了一下加上动态调试发现0x14000E55C很可能是一个strcpy函数
然后main函数如下:
if ( argc == 2 )
{
my_strcpy(input, 260i64, argv[1], 260i64);
check(&retaddr);
result = 0;
}
else
{
//else里面的代码也很恐怖
//但是题目明显参数不为2就会给出"input like this:crackme.exe mykey"的提示
//猜测是一个动态生成字符串并输出的功能
}
strcpy会把输入复制到一个全局变量,然后check会去访问它,并且校验。
回到看起来很难逆check函数,思路是,不可能一行行逆这个东西,要找关键代码,即,它在哪里访问了输入,怎么访问的?说的高端点,叫做静态污点分析。
check 1
那么直接定位到访问input的地方,首先我看到的是这个:
if ( sub_140002AC0(iter) == -5808510693665524758i64 )// check [0:9]
{
iter[0] = input[1];
if ( sub_140002AC0(iter) == -5808494200991101593i64 )
{
iter[0] = input[2];
if ( sub_140002AC0(iter) == -5808519489758550446i64 )
{
iter[0] = input[3];
if ( sub_140002AC0(iter) == -5808507395130640125i64 )
{
iter[0] = input[4];
if ( sub_140002AC0(iter) == -5808522788293435079i64 )
{
iter[0] = input[5];
if ( sub_140002AC0(iter) == -5808606351177179115i64 )
{
iter[0] = input[6];
if ( sub_140002AC0(iter) == -5808608550200435537i64 )
{
iter[0] = input[7];
if ( sub_140002AC0(iter) == -5808609649712063748i64 )
{
iter[0] = input[8];
if ( sub_140002AC0(iter) == -5808599754107409849i64 )
{
v42 = 0i64;
v37 = 0;
v43 = 329472i64;
}
else
{
v42 = 0x100000000i64;
v43 = 3750656i64;
}
//其他else一样
很有可能是一个校验,动态调试也发现input并未被改变,那么逐字节爆破
signed __int64 __fastcall sub_140002AC0(char *a1)
{
char v1; // dl
signed __int64 result; // rax
signed __int64 v3; // rax
v1 = *a1;
for (result = -3750763034362895579i64; *a1; result = 1099511628211i64 * v3)
{
++a1;
v3 = result ^ v1;
v1 = *a1;
}
return result;
}
signed __int64 arr[9] = { -5808510693665524758i64,
-5808494200991101593i64,
-5808519489758550446i64,
-5808507395130640125i64,
-5808522788293435079i64,
-5808606351177179115i64,
-5808608550200435537i64,
-5808609649712063748i64,
-5808599754107409849i64 };
for (size_t i = 0; i < 9; i++)
{
for (size_t j = 0; j < 256; j++)
{
if (sub_140002AC0((char*)(&j)) == arr[i])
{
printf("%c", (char)j);
}
}
}
得出答案为KXCTF2018,试一下发现不对(怎么可能这么简单
check 2
接下来看到的是一个疑似长度的校验
do
++len;
while ( input[len] );
LODWORD(v307) = len;
HIDWORD(v307) = 5 - HIDWORD(len);
v529 = ((unsigned int)len + ((unsigned __int64)HIDWORD(len) << 32)) >> 12;
//无用代码,稍微有点长
if ( (len & 0xFFF) + (v529 << 12) == 30 )
//...
看起来很复杂的操作,但是稍微看一下加上动态调试一下的话,就能发现这是在检查长度为30
check 3
紧接着看后面的代码,又访问了input
if ( (len & 0xFFF) + (v529 << 12) == 30 )
{
v8 = 0;
i_1 = 0;
if ( len )
{
iter_input = input;
do
{
v184 = 0;
v11 = *iter_input;
LOBYTE(v184) = v11;
v12 = (char *)&v184;
v13 = -3750763034362895579i64;
if ( v11 )
{
do
{
v13 = 1099511628211i64 * (v13 ^ v11);
v11 = *++v12;
}//实际上是内联了sub_140002AC0
while ( *v12 );
}
v14 = v8 + 1;
if ( v13 != -5808600853619038060i64 )
v14 = v8;
v8 = v14;
++i_1;
++iter_input;
}
while ( i_1 < len );
v1 = 32i64;
if ( v14 >= 3 ) // 3个以上的9
{
//...
}
大概逻辑是,满足sub_140002AC0((char*)(&j)) == -5808600853619038060i64的,要有3个以上。
同样爆破一下
for (size_t j = 0; j < 256; j++)
{
if (sub_140002AC0((char*)(&j)) == check3more)
{
printf("%c", (char)j);
}
}
结果为9
check 4
if ( sub_140002AC0(input) == 5728707748789076223i64 )// check whole input
{
LODWORD(v46) = 31;
LODWORD(v47) = 0;
}
整个input的hash(sub_140002AC0貌似是一个hash函数,但是又没有雪崩效应)的检查,很明显现在条件不足。
check 5
其实到现在为止,还并没有找到爆破点,只是一直在“猜”。因为一般来讲,一个判断,更加复杂(明显有更复杂的代码)或者更难到达(比方说等于情况,任意输入一般都是不等于的可能性要更大)的一条路径,是正确答案所需要被走到的。前面几个check,也是根据这个“猜”的。
现在走下一个check,
memset(dll_name, 0i64, 0x104ui64);
v80 = strstr(input, "9");
v81 = v80;
dll_name[0] = v80[1];
dll_name[1] = v80[2];
dll_name[2] = v80[3];
dll_name[3] = v80[4];
dll_name[4] = v80[5]; // 第一个9后面取5个字节
mystrcat(dll_name, 260ui64, ".DLL", 260ui64);
v82 = strstr(v81 + 1, "9");
snd9 = v82;
*strstr(v82 + 1, "9") = 0;
memset(&proc_name, 0i64, 260ui64);
my_strcpy(&proc_name, 260i64, snd9 + 1, 260i64); //取第二个9到第三个9之间的字符串
其中这些函数的识别,肯定不是通过静态逆向,点进去发现一堆SSE就能吓跑人了,识别是通过猜+调试。
然后这刚好对应大于3个9的check,说明应该没逆错。
其中有个strcat把.DLL加在后面,难道是把输入作为一个dll名?看了一下发现题目并没有给出dll附件。
那么继续看先吧,看看这两个参数是怎么被用的。这后面又有大量的代码,不管它们,用xref。发现一个调用
module_addr = ((__int64 (__fastcall *)(char *))((char *)v94 + *(unsigned int *)((char *)&v94[*(unsigned __int16 *)((char *)v94 + 2 * v98 + (unsigned int)v95[9])] + (unsigned int)v95[7])))(dll_name);
::module_addr = module_addr;
函数地址貌似是动态生成的,猜测这是一个LoadLibrary函数,x64dbg一看,果然是。
然后proc_name的下一次被使用(当然现在还不知道是proc_name
v116 = (__int64 (__fastcall *)(_QWORD, signed __int64))((__int64 (__fastcall *)(__int64, char *))((char *)v109 + *(unsigned int *)((char *)&v109[*(unsigned __int16 *)((char *)v109 + 2 * v111 + (unsigned int)v110[9])] + (unsigned int)v110[7])))( module_addr, &proc_name); // GetProcAddr
if ( v116 )
{
len_1 = -1i64;
do
++len_1;
while ( input[len_1] );
critical = v116(0i64, len_1);
}
else
{
//...大量垃圾代码
}
if ( critical >= 0 )
{
//错误
}
else
{
//正确
}
动态调试,发现call的函数是GetProcAddr。然后critical是关键,动态调试改eflags发现后面如果critical<0,可以显示正确信息。而如果GetProcAddr返回NULL,else分支中永远会产生>=0的critical值,所以明显是要给个能用的dll名和相应的函数名,使得其调用返回负数。
所以flag大概应该是 KXCTF20189AAAAA9XXXXXXXXXXXXX9。
但是,windows的API,一般都是返回0或正的,错误返回负那是Linux的API。这里我又卡了很久,想了好多种方法,最后决定还是猜一猜。首先dll名要是5的长度,第一个想到的就是ntdll.dll,然后去System32目录下找,发现还有一个是wow64.dll,其他的感觉有点偏,应该不会用上(目前先这么猜
然后把所有长度为13的导出函数弄下来,我用的是IDA(LordPE啥的并没有办法复制...
然后通过check 4的那个hash值,来判断。(就先不管负不负了
然后注意,windows文件系统不分大小写,所以ntdll或者NTDLL甚至NtDlL都可以,这个坑了我很久,也是后面才想到的。
具体爆破代码:
const char* funcs[81] = { "TppTimerpFree","TpReleaseWork","TpReleaseWait","RtlAreBitsSet","LdrpUnloadDll","LdrpSnapThunk","RtlUnlockHeap","RtlLoadString","TppTimerAlloc","EtwEventWrite","RtlStartRXact","RtlAbortRXact","TpWaitForWait","RealSuccessor","RebalanceNode","RtlGetVersion","RtlpNtOpenKey","RtlCopyString","RtlSetAllBits","RtlFreeHandle","ZwQueryObject","NtOpenProcess","NtOpenSection","ZwCreateEvent","ZwSetValueKey","NtCancelTimer","ZwAccessCheck","NtAlertThread","NtCompactKeys","NtCompressKey","ZwConnectPort","ZwCreateTimer","NtCreateToken","ZwFilterToken","ZwOpenSession","ZwQueryEaFile","ZwQueryMutant","NtRequestPort","NtSetUuidSeed","ZwStopProfile","ZwUnloadKeyEx","DbgBreakPoint","DebugService2","RtlFillMemory","RtlZeroMemory","StringCbCopyW","RtlRemoteCall","LdrpCreateKey","PfxFindPrefix","PfxInitialize","IsTimeExpired","WaitForWerSvc","WerpProcessId","DbgUiContinue","RtlpLockStack","RtlApplyRXact","TpReleasePool","TpWaitForWork","LdrReadMemory","ResCHitsFlush","RtlCreateHeap","RtlIdnToAscii","RtlpTpIoAlloc","LdrResRelease","Wow64LogPrint","NameToOrdinal","Wow64FreeHeap","whNtWriteFile","whNtReplyPort","whNtCreateKey","whNtOpenEvent","whNtDeleteKey","whNtLoadKeyEx","whNtOpenKeyEx","whNtOpenTimer","whNtRenameKey","whNtSaveKeyEx","whNtSetEaFile","whNtTestAlert","whNtUnloadKey","Wow64pLongJmp" };
const char* libs[] = { "NTDLL", "WOW64" };
char flag[31] = "KXCTF20189AAAAA9XXXXXXXXXXXXX9";
for (size_t l = 0; l < 2; l++)
{
memcpy(flag + 10, libs[l], 5);
for (size_t b = 0; b < 32; b++)
{
for (size_t s = 0; s < 5; s++)
{
if (b & (1 << s))
{
flag[10 + s] |= 0x20;//转成小写
}
}
for (size_t i = 0; i < 64; i++)
{
memcpy(flag + 16, funcs[i], 13);
if (sub_140002AC0(flag) == 5728707748789076223i64)
{
printf("%s", flag);
}
}
}
}
最终输出为KXCTF20189NTDLL9DbgUiContinue9
实际上,flag并不一定是要我所说的那种形式,KXCTF20189AAAAA9...9...也是有可能的,其中函数名长度小于13,然后后面的找一个序列使得check 4哈希碰撞,毕竟那么多长度为5的dll,还有那么多导出函数,有没有这种可能性呢?(我是懒的弄了...
合作伙伴
京东集团是中国收入最大的互联网企业之一,于2014年5月在美国纳斯达克证券交易所正式挂牌上市,业务涉及电商、金融和物流三大板块。
京东是一家技术驱动成长的公司,并发布了“第四次零售革命”下的京东技术发展战略。信息安全作为保障业务发展顺利进行的基石发挥着举足轻重的作用。为此,京东信息安全部从成立伊始就投入大量技术和资源,支撑京东全业务线安全发展,为用户、供应商和京东打造强大的安全防护盾。
随着京东全面走向技术化,大力发展人工智能、大数据、机器自动化等技术,将过去十余年积累的技术与运营优势全面升级。面向AI安全、IoT安全、云安全的机遇及挑战,京东安全积极布局全球化背景下的安全人才,开展前瞻性技术研究,成立了硅谷研发中心、安全攻防实验室等,并且与全球AI安全领域知名的高校、研究机构建立了深度合作。
京东不仅积极践行企业安全责任,同时希望以中立、开放、共赢的态度,与友商、行业、高校、政府等共同建设互联网安全生态,促进整个互联网的安全发展。
CTF 旗帜已经升起,等你来战!
扫描二维码,立即参战!
✎看雪.京东 2018 CTF
看雪.京东 2018 CTF 第八题 薛定谔之猫 点评与解析
看雪2018安全开发者峰会
2018年7月21日,拥有18年悠久历史的老牌安全技术社区——看雪学院联手国内最大开发者社区CSDN,倾力打造一场技术干货的饕餮盛宴——2018 安全开发者峰会,将在国家会议中心隆重举行。会议面向开发者、安全人员及高端技术从业人员,是国内开发者与安全人才的年度盛事。此外峰会将展现当前最新、最前沿技术成果,汇聚年度最强实践案例,为中国软件开发者们呈献了一份年度技术实战解析全景图。
戳下图↓,立即购票,享5折优惠!